6.9 Erweiterung der Klassenhierarchie »CircleApplication«  
Wir wollen uns nun noch einmal dem von uns immer weiter entwickelten Beispielprojekt CircleApplication zuwenden, den Entwurf um zwei weitere Klassen, nämlich um Rectangle und GraphicRectangle, ergänzen und uns dabei die in diesem Kapitel gewonnenen Kenntnisse zunutze machen. Die Klasse Rectangle soll ein Rechteck beschreiben und die Klasse GraphicRectangle eine Operation bereitstellen, um ein Rectangle-Objekt in einer grafikfähigen Komponente darzustellen – analog zur Klasse GraphicCircle.
Ebenso wie ein Circle-Objekt soll auch ein Rectangle-Objekt seine Lage beschreiben. Dazu wird ein Punkt vom Typ Point mit seinen x- und y-Koordinaten definiert. Um bei der üblichen Konvention grafischer Benutzeroberflächen zu bleiben, soll es sich dabei um den oberen linken Punkt des Rechtecks handeln. Die Größe eines Rechtecks wird durch seine Breite und Länge definiert. Außerdem sind Methoden vorzusehen, um Umfang und Fläche zu berechnen und zwei Rectangle-Objekte zu vergleichen.
Die Klassendefinition Rectangle könnte dann wie folgt aussehen:
| ' ---------- Delegate ----------
|
| Public Delegate Sub MeasureErrorRectangleEventHandler(ByVal rect _
|
| As Rectangle)
|
| Public Class Rectangle
|
| Implements IDisposable
|
| ' ---------- Ereignisse ----------
|
| Public Event MeasureError As MeasureErrorRectangleEventHandler
|
| ' ---------- statische Felder ----------
|
| Protected Shared intCountRectangles As Integer
|
| ' ---------- Felder ----------
|
| Protected ptPosition As New Point
|
| Protected dblBreite As Double = 0
|
| Protected dblLaenge As Double = 0
|
| Private bolCounterReduced As Boolean = False
|
| ' ---------- Konstruktoren ----------
|
| Public Sub New()
|
| intCountRectangles += 1
|
| End Sub
|
| Public Sub New(ByVal laenge As Double, ByVal breite As Double)
|
| Me.New()
|
| Me.breite = breite
|
| Me.laenge = laenge
|
| End Sub
|
| Public Sub New(ByVal x As Integer, ByVal y As Integer, _
|
| ByVal laenge As Double, ByVal breite As Double)
|
| Me.New(laenge, breite)
|
| Me.ptPosition.X = x
|
| Me.ptPosition.Y = y
|
| Me.Breite = breite
|
| Me.Laenge = laenge
|
| End Sub
|
| Public Sub New(ByVal laenge As Double, _
|
| ByVal breite As Double, ByVal pt As Point)
|
| Me.new(Laenge, Breite)
|
| Me.position = pt
|
| End Sub
|
| ' --------- Finalizer --------------------
|
| Protected Overrides Sub Finalize()
|
| If Not bolCounterReduced Then
|
| intCountRectangles -= 1
|
| End If
|
| End Sub
|
| Public Sub Dispose() Implements IDisposable.Dispose
|
| intCountRectangles -= 1
|
| bolCounterReduced = True
|
| End Sub
|
| ' ---------- Eigenschaften ----------
|
| Public Property Breite() As Double
|
| Get
|
| Return Me.dblBreite
|
| End Get
|
| Set(ByVal value As Double)
|
| If (value >= 0) Then
|
| Me.dblBreite = value
|
| Else
|
| RaiseEvent MeasureError(Me)
|
| End If
|
| End Set
|
| End Property
|
| Public Property Laenge() As Double
|
| Get
|
| Return Me.dblLaenge
|
| End Get
|
| Set(ByVal value As Double)
|
| If (value >= 0) Then
|
| Me.dblLaenge = value
|
| Else
|
| RaiseEvent MeasureError(Me)
|
| End If
|
| End Set
|
| End Property
|
| Public Property Position() As Point
|
| Get
|
| Return Me.ptPosition
|
| End Get
|
| Set(ByVal value As Point)
|
| Me.ptPosition = value
|
| End Set
|
| End Property
|
| ' ---------- Instanzmethoden ----------
|
| Public Function GetFlaeche() As Double
|
| Return Me.dblLaenge * Me.dblBreite
|
| End Function
|
| Public Function GetUmfang() As Double
|
| Return 2 * (Me.dblLaenge + Me.dblBreite)
|
| End Function
|
| Public Function Bigger(ByVal rect As Rectangle) _
|
| As Rectangle
|
| If (Me.dblLaenge * Me.dblBreite >= _
|
| rect.dblLaenge * rect.dblBreite) Then
|
| Return Me
|
| End If
|
| Return rect
|
| End Function
|
| Public Function Bigger(ByVal rect As Rectangle, _
|
| ByRef equal As Boolean) As Rectangle
|
| equal = False
|
| If (Me.dblLaenge * Me.dblBreite = _
|
| rect.dblLaenge * rect.dblBreite) Then
|
| equal = True
|
| End If
|
| Return Me.Bigger(rect)
|
| End Function
|
| Public Sub MoveXY(ByVal newCenterPoint As Point)
|
| Me.ptPosition = newCenterPoint
|
| End Sub
|
| ' ---------- Klassenmethoden -----------
|
| Public Shared Function GetFlaeche(ByVal laenge As Double, _
|
| ByVal breite As Double) As Double
|
| Return laenge * breite
|
| End Function
|
| Public Shared Function GetUmfang(ByVal laenge As Double, _
|
| ByVal breite As Double) As Double
|
| Return 2 * (laenge + breite)
|
| End Function
|
| Public Shared Function Bigger(ByVal rect1 As Rectangle, _
|
| ByVal rect2 As Rectangle) As Rectangle
|
| If (rect1.Breite * rect1.Laenge >= _
|
| rect2.Laenge * rect2.Breite) Then
|
| Return rect1
|
| End If
|
| Return rect2
|
| End Function
|
| Public Shared Function Bigger(ByVal rect1 As Rectangle, _
|
| ByVal rect2 As Rectangle, ByVal equal As Boolean) As Rectangle
|
| equal = False
|
| If (rect1.Breite * rect1.Laenge = _
|
| rect2.Laenge * rect2.Breite) Then
|
| equal = True
|
| End If
|
| Return Rectangle.Bigger(rect1, rect2)
|
| End Function
|
| Public Shared Function IsBigger(ByVal rect1 As Rectangle, _
|
| ByVal rect2 As Rectangle) As Boolean
|
| If (rect1.breite * rect1.laenge >= _
|
| rect2.laenge * rect2.breite) Then
|
| Return True
|
| End If
|
| Return False
|
| End Function
|
| ' ---------- Klasseneigenschaften ----------
|
| Public Shared ReadOnly Property CountRectangles() As Integer
|
| Get
|
| Return Rectangle.intCountRectangles
|
| End Get
|
| End Property
|
| End Class
|
Und nun auch noch der Code der Klasse GraphicRectangle:
| Public Class GraphicRectangle
|
| Inherits Rectangle
|
| ' Konstruktoren
|
| Public Sub New()
|
| MyBase.New()
|
| End Sub
|
| Public Sub New(ByVal laenge As Double, ByVal breite As Double)
|
| MyBase.New(laenge, breite)
|
| End Sub
|
| Public Sub New(ByVal x As Integer, ByVal y As Integer, _
|
| ByVal laenge As Double, ByVal breite As Double)
|
| MyBase.New(x, y, laenge, breite)
|
| End Sub
|
| Public Sub New(ByVal laenge As Double, _
|
| ByVal breite As Double, ByVal pt As Point)
|
| MyBase.New(laenge, breite, pt)
|
| End Sub
|
| ' Typspezifische Methode
|
| Public Sub Draw()
|
| Console.WriteLine("Das Rechteck wird gezeichnet")
|
| End Sub
|
| End Class
|
Es ist deutlich zu erkennen, dass sich die Klassen Rectangle und Circle ähneln, ebenso die beiden Klassen GraphicCircle und GraphicRectangle. Dies spricht dafür, allen vier Klassen eine Basisklasse vorzuschalten, welche die gemeinsamen Merkmale eines Kreises und eines Rechtecks beschreibt: Wir werden diese Klasse im Folgenden GeometricObject nennen.
Ein weiteres Argument für diese Lösung ist die sich daraus ergebende Gleichnamigkeit der gemeinsamen Merkmale: Es werden dann die Methoden, die ihren Fähigkeiten nach Gleiches leisten, unabhängig vom Typ des zugrunde liegenden Objekts in gleicher Weise aufgerufen. Einerseits lässt sich dadurch die abstrahierte Artverwandtschaft der beiden geometrischen Objekte Kreis und Rechteck verdeutlichen, andererseits wird die Benutzung der Klassen wesentlich vereinfacht, da nicht zwei unterschiedliche Methodennamen dasselbe Leistungsmerkmal beschreiben.
 Hier klicken, um das Bild zu Vergrößern
Abbildung 6.6 Die Klassenhierarchie des Projekts »CircleApplication«
Nach diesen ersten Überlegungen soll nun die Klasse GeometricObject implementiert werden.
6.9.1 Die Klasse »GeometricObject«  
Vergleichen wir jetzt Schritt für Schritt die einzelnen Klassenmitglieder von Circle und Rectangle, um daraus ein einheitliches Konzept für den Entwurf des Oberbegriffs GeometricObject zu formulieren.
Instanzvariablen und Eigenschaftsmethoden
Ein Circle-Objekt wird nach dem augenblicklichen Stand durch seinen Radius und ein Point-Objekt namens center beschrieben, dessen Koordinaten über die Eigenschaftsmethoden XKoordinate und YKoordinate festgelegt werden.
Auch zur Positionsbeschreibung eines Rechtecks dient ein Point-Objekt. Allerdings kann nach der augenblicklichen Implementierung die Positionierung nur über die direkte Übergabe eines Point-Objekts an die Eigenschaft Position erfolgen.
Obwohl sich beide Klassendefinitionen hinsichtlich der Positionsdatenübergabe unterscheiden, bietet es sich uns an, jeden der beiden Typen sowohl mit einer Eigenschaft vom Typ Point als auch mit Eigenschaften, die das Festlegen der Koordinaten in X- und in Y-Richtung ermöglichen, auszustatten. Mit dieser ergänzenden Maßnahme erhöhen wir für jeden Typ sogar noch die Flexibilität, ohne dabei Einbußen in Kauf nehmen zu müssen. Wir können also alle mit der Positionierung im Zusammenhang stehenden Klassenelemente in die Basisklasse GeometricObject auslagern.
Der Radius eines Circle-Objekts sowie die Länge und die Breite eines Rectangle-Objekts sind objektspezifische Daten. Diese Instanzvariablen lassen wir in den ursprünglichen Klassendefinitionen.
Beide Klassendefinitionen definieren außerdem das private Feld counterReduced, mit dem der Objektzähler beim Zerstören des Objekts mit Dispose und dem Destruktor gesteuert wird. Unter Änderung des Zugriffsmodifizierers in Protected ist auch dieses Datenmember ein erstklassiger Kandidat für die neu geschaffene Basisklasse.
| ' ---------- Instanzvariablen in GeometricObject ----------
|
| Protected center As New Point
|
| Protected bolCounterReduced As Boolean = False
|
| ' ---------- Eigenschaftsmethoden ----------
|
| Public Property XKoordinate() As Integer
|
| Get
|
| Return center.X
|
| End Get
|
| Set(ByVal Value As Integer)
|
| center.X = Value
|
| End Set
|
| End Property
|
| Public Property YKoordinate() As Integer
|
| Get
|
| Return center.Y
|
| End Get
|
| Set(ByVal Value As Integer)
|
| ´ center.Y = Value
|
| End Set
|
| End Property
|
| Public Property Position() As Point
|
| Get
|
| Return Me.center
|
| End Get
|
| Set(ByVal value As Point)
|
| Me.center = value
|
| End Set
|
| End Property
|
Konstruktoren und Destruktoren
Da sich Konstruktoren und Destruktoren nicht an die abgeleiteten Klassen vererben, bleiben die Erstellungs- und Zerstörungsroutinen in Circle und Rectangle unverändert. Ein eigener Konstruktor in GeometricObject ist nicht notwendig, weder der parameterlose noch ein parametrisierter.
Die Instanzmethoden
Ein Vergleich hinsichtlich der Instanzmethoden beider Klassen führt zu der Erkenntnis, dass beide Klassen eine gleichnamige überladene Methode Bigger veröffentlichen, die zwei Objekte miteinander vergleicht und die Referenz auf das größere der verglichenen Objekte als Resultat des Methodenaufrufs an den Benutzer zurückgibt. Aus logischer Sicht erbringen diese Methoden sowohl in Circle als auch in Rectangle dieselbe Leistung, unterscheiden sich nur im Typ des Parameters bzw. des Rückgabewerts: Die Bigger-Methode in der Circle-Klasse nimmt die Referenz auf ein Kreisobjekt entgegen, die Klasse Rectangle die Referenz auf ein Rectangle-Objekt.
Wir können uns den Umstand zunutze machen, dass sowohl die Circle- als auch die Rectangle-Klasse aus derselben Basisklasse abgeleitet werden, und müssen dazu nur den Typ des Parameters und der Rückgabe entsprechend anpassen:
| ' ----- Instanzmethoden -----
|
| Public Function Bigger(ByVal obj As GeometricObject) _
|
| As GeometricObject
|
| If Me.GetFlaeche >= obj.GetFlaeche Then
|
| Return Me
|
| End If
|
| Return obj
|
| End Function
|
| Public Function Bigger(ByVal obj As GeometricObject, _
|
| ByRef equal As Boolean) As GeometricObject
|
| equal = False
|
| If Me.GetFlaeche = obj.GetFlaeche Then
|
| equal = True
|
| End If
|
| Return Me.Bigger(obj)
|
| End Function
|
Weil Circle und Rectangle von GeometricObject abgeleitet werden, gilt, dass sowohl ein Circle-Objekt als auch Rectangle-Objekt gleichzeitig auch ein Objekt vom Typ GeometricObject ist. Beide Methoden werden unter Übergabe einer spezifischen Objektreferenz aufgerufen, welche die Laufzeitumgebung implizit konvertiert. Als Nebeneffekt beschert uns diese Verallgemeinerung, dass wir nun in der Lage sind, die Flächen von zwei verschiedenen Typen zu vergleichen, denn nun kann die Bigger-Methode auf eine Circle-Referenz aufgerufen und als Argument die Referenz auf ein Rectangle-Objekt übergeben werden.
In der Bigger-Methode wird GetFlaeche aufgerufen, die natürlich weiterhin typgebunden ist, da die Fläche auf die typspezifischen Daten Radius bzw. Laenge und Breite zugreift. Es liegt aber nahe, GetFlaeche als abstrakte Methode festzulegen, die von den ableitenden Klassen Circle und Rectangle überschrieben werden muss. Polymorph wird dann der Zugriff auf die richtige Implementierung sichergestellt. Mit derselben Argumentation kann auch GetUmfang als abstrakte Methode in GeometricObject definiert werden.
| ' ---------- abstrakte Instanzmethoden ---------------
|
| Public MustOverride Function GetFlaeche() As Double
|
| Public MustOverride Function GetUmfang() As Double
|
MoveXY ist im Vergleich zu den beiden vorgenannten Methoden typunabhängig und kann daher vollständig in GeometricObject implementiert werden.
| Public Sub MoveXY(ByVal newCenterPoint As Point)
|
| Me.center = newCenterPoint
|
| End Sub
|
Die Klassenmethoden
Die Argumentation, die uns dazu brachte, die Instanzmethode Bigger in der Basisklasse zu codieren, kann auch bei den Klassenmethoden Bigger und IsBigger geführt werden. Wir müssen jeweils nur den Typ des Parameters und bei der Methode Bigger auch den des Rückgabewerts ändern, um die Klassenmethoden in der Klasse GeometricObject bereitzustellen.
| ' ---------- Klassenmethoden in GeometricObject -----------
|
| Public Shared Function Bigger(ByVal obj1 As GeometricObject, _
|
| ByVal obj2 As GeometricObject) As GeometricObject
|
| If (obj1.GetFlaeche() >= obj2.GetFlaeche()) Then
|
| Return obj1
|
| End If
|
| Return obj2
|
| End Function
|
| Public Shared Function Bigger(ByVal obj1 As GeometricObject, _
|
| ByVal obj2 As GeometricObject, ByRef equal As Boolean) _
|
| As GeometricObject
|
| equal = False
|
| If (obj1.GetFlaeche() = obj2.GetFlaeche()) Then
|
| equal = True
|
| End If
|
| Return GeometricObject.Bigger(obj1, obj2)
|
| End Function
|
| Public Shared Function IsBigger(ByVal obj1 As GeometricObject, _
|
| ByVal obj2 As GeometricObject) As Boolean
|
| If (obj1.GetFlaeche() >= obj1.GetFlaeche()) Then
|
| Return True
|
| End If
|
| Return False
|
| End Function
|
Das Ereignis »MeasureError«
Wir wollen nun auch das Ereignis MearureError in GeometricObject implementieren und es allen abgeleiteten Klassen bereitstellen. Das ist jedoch nicht ganz so einfach und verlangt eine genauere Analyse, wie Sie noch sehen werden.
Rufen wir uns zuerst die Implementierung des Ereignisses in der Klasse Circle in Erinnerung:
| Public Event MeasureError(ByVal k As Circle)
|
Ausgelöst wird das Ereignis MeasureError in der Eigenschaftsmethode Radius mit:
| Public Property Radius() As Double
|
| Get
|
| Return dblRadius
|
| End Get
|
| Set(ByVal Value As Double)
|
| If Value >= 0 Then
|
| dblRadius = Value
|
| Else
|
| RaiseEvent MeasureError(Me)
|
| End If
|
| End Set
|
| End Property
|
Der Übergabeparameter des Delegaten vom Typ Circle versetzt den Benutzer der Klasse in die Lage, bei einer unzulässigen Zuweisung an Radius im Ereignishandler auf die Objektreferenz den Radius neu einzugeben, beispielsweise:
| Public Sub SetNewRadius(sender As Circle)
|
| Console.WriteLine("Unzulässige Eingabe des Radius.")
|
| Console.Write("Neueingabe: ")
|
| sender.Radius = Console.ReadLine()
|
| End Sub
|
Ähnlich ist der Sachverhalt in Rectangle. Diese Klasse stellt auch ein Ereignis MeasureError bereit, dieses ist jedoch vom Typ MeasureErrorRectangleEventHandler.
Das Ereignis wird, ähnlich wie in der Klasse Circle, ausgelöst, wenn einer der beiden Seitenlängen ein negativer Wert zugewiesen wird.
MeasureError soll in der Klasse GeometricObject als Ereignis dienen, wenn entweder einem Circle- oder einem Rectangle-Objekt ein unzulässiger Wert übergeben wird. Damit wird sofort die erste Konsequenz klar: Da wir weiterhin davon ausgehen, dass bei Ereignisauslösung in einem Circle-Objekt dieses die Referenz auf sich selbst an den Ereignishandler übergeben will, müssen wir den Typ in der Parameterliste des Delegaten entsprechend anpassen. Ein Rectangle-Objekt, dessen Ereignis bisher noch nicht die Referenz auf sich selbst dem Ereignishandler mitteilte, kann davon nur profitieren.
| ' ---------- vorläufiger Delegat in GeometricObject --------
|
| Public Delegate Sub MeasureErrorEventHandler
|
| ´ (ByVal geo As GeometricObject)
|
Wir lassen zunächst alle anderen Aspekte hinsichtlich der Implementierung in den Klassen außen vor und stellen weitere Überlegungen an. Wird MeasureError bei dieser Definition in einem Circle-Objekt ausgelöst, kann im Ereignishandler der Radius nach vorheriger Konvertierung der Referenz sender neu zugewiesen werden, z. B.:
| Public Sub SetNewMeasure(ByVal sender As GeometricObject)
|
| ...
|
| sender.Radius = Console.ReadLine()
|
| End Sub
|
Etwas anders verhält es sich jedoch, wenn entweder die Breite oder die Länge eines Rechtecks nicht den Vorgaben entspricht. Woher soll der Ereignishandler die Information nehmen, welcher Eigenschaft ein unzulässiger, negativer Wert zugewiesen worden ist? Es stehen keinerlei Informationen darüber zur Verfügung, ob es sich um Breite oder Laenge gehandelt hat.
Wir sollten eine Lösung für dieses Problem finden, denn dasselbe Ereignis bei der Auslösung durch ein Circle-Objekt anders zu behandeln als bei der Auslösung durch ein Rectangle-Objekt wäre eine schlechte Alternative. Die Lösung muss für beide in Frage kommenden Typen zu einer gleichwertigen Behandlung führen.
Der Ansatz, der uns zum Ziel führt, geht über die Definition eines weiteren Parameters, der im Ereignishandler die Referenz auf den Wert liefert, der fälschlicherweise zugewiesen wurde.
| Public Delegate Sub MeasureErrorEventHandler
|
| (ByVal geo As GeometricObject, ByRef measure As Double)
|
Der Ereignishandler in einer Clientklasse, der die Korrektur durch den Anwender zur Laufzeit zulässt, könnte im einfachsten Fall wie folgt codiert werden:
| ' Ereignishandler in der Clientklasse
|
| Public Sub SetNewMeasure(
|
| ByVal sender As GeometricObject, ByRef measure As Double)
|
| measure = Console.ReadLine()
|
| End Sub
|
Sollte es sich als notwendig erweisen, kann die Referenz sender daraufhin untersucht werden, ob der Auslöser ein Circle- oder ein Rectangle-Objekt war. Aber auch ohne diese Unterscheidung wird der Referenzparameter den neuen Wert immer richtig an die Ereignisquelle weiterleiten.
Wir haben das endgültige Ziel nahezu erreicht, müssen aber jetzt noch dem Umstand Beachtung schenken, dass Ereignisse in der Klasse ausgelöst werden, in der sie definiert sind. Damit das in einer Basisklasse definierte Ereignis auch für den Code in einer abgeleiteten Klasse zugänglich ist, muss die Basisklasse GeometricObject nicht nur das Ereignis MeasureError, sondern auch eine Methode bereitstellen, die in der Art eines Adapters die Verbindung zwischen einer Sub- und der Basisklasse sicherstellt, wenn die abgeleitete Klasse das Ereignis auslöst:
| Public Sub OnMeasureError(ByVal obj As GeometricObject, _
|
| ByVal measure As Double)
|
| RaiseEvent MeasureError(obj, measure)
|
| End Sub
|
Objekte der abgeleiteten Klassen Circle und Rectangle müssen jetzt die Methode OnMeasureError aufrufen und neben dem Me-Zeiger auch den Wert als Referenz übergeben, der die Ablehnung im Objekt verursacht hat. Behandelt der Client das Ereignis und weist einen neuen Wert zu, wird über OnMeasureError der korrigierte Wert an das Objekt der abgeleiteten Klasse weitergeleitet. Hier kann nun eine erneute Überprüfung durch einen weiteren Aufruf der entsprechenden Ereignismethode erfolgen. Beispielhaft sei das an der Eigenschaft Radius in der Klasse Circle gezeigt:
| Public Property Radius() As Double
|
| Get
|
| Return dblRadius
|
| End Get
|
| Set(ByVal Value As Double)
|
| If Value >= 0 Then
|
| dblRadius = Value
|
| Else
|
| Me.OnMeasureError(Me, Value)
|
| End If
|
| End Set
|
| End Property
|
| Anmerkung
|
|
Sie finden den vollständigen Code zu diesem Beispiel auf der Buch-CD unter ...\Kapitel 6\CircleApplication_5.
|
|